实战FreeRTOS的UsageFault异常
功能方面,它也有很多玩法,闪存、USB、GPIO,CoreSight技术等等,都值得探寻。
当然,如果给它加上操作系统,就更有意思了。
ARM-M核是典型的MCU,它上面的操作系统有个统一的称呼,一般称为RTOS(实时操作系统)。
在众多的RTOS中,FreeRTOS是流行度很高的一种。在
sourceforge的2022 RTOS排名中(Compare the Top Real-Time Operating Systems (RTOS) of 2022)位居首位。
lk!EXTI2_IRQHandler [../../startup/startup_GDK3.s @ 78]:
8001c50 e7fe b #0x8001c50
比较其它版本的代码,发现有些版本中有如下宏定义:
/* Corresponding to startup.s */
#define vPortSVCHandler SVC_Handler
#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler SysTick_Handler
上面的宏是把代码里的函数名重定义为标准的名字。按一般的C语言习惯,这种用法是有点古怪的。但是实际效果是有效的。
加上这几个宏,再编译更新,使用NDB的dds命令观察IVT,可以看到上面三个宏相关的中断处理函数都换成新的了。
dds 000000000 20005000
00000004 08001c0d lk!Reset_Handler+0x1 [../../startup/startup_GDK3.s @ 39]
00000008 08001c51 lk!EXTI2_IRQHandler+0x1 [../../startup/startup_GDK3.s @ 78]
0000000c 08001b7d lk!HardFault_Handler+0x1 [../../startup/exceptions.c @ 279]
00000010 08001ba1 lk!MemManage_Handler+0x1 [../../startup/exceptions.c @ 294]
00000014 08001bc5 lk!BusFault_Handler+0x1 [../../startup/exceptions.c @ 309]
00000018 08001be9 lk!UsageFault_Handler+0x1 [../../startup/exceptions.c @ 324]
0000001c 00000000
00000020 00000000
00000024 00000000
00000028 00000000
0000002c 08001441 lk!SVC_Handler+0x1 [portable/RVDS/ARM_CM3/port.c @ 229]
00000030 08001c51 lk!EXTI2_IRQHandler+0x1 [../../startup/startup_GDK3.s @ 78]
00000034 00000000
00000038 08001551 lk!PendSV_Handler+0x1 [portable/RVDS/ARM_CM3/port.c @ 404]
0000003c 080015ad lk!SysTick_Handler+0x1 [portable/RVDS/ARM_CM3/port.c @ 441]
00000040 08001c51 lk!EXTI2_IRQHandler+0x1 [../../startup/startup_GDK3.s @ 78]
更重要的是,有了这个修改后,两个示例线程开始跑了,在调试器里可以看到线程工作函数开头的printf函数打印的信息。
gdk3:FreeRTOS Kernel Version:V10.4.4+
gdk3:task1 entry
gdk3:task2 entry
可是,进展就到这里,接下来就出异常了。
gdk3:Type: Usage Fault
gdk3:Reason: invalid EPSR
gdk3:
gdk3:R0=0 R1=2000108c
gdk3:R2=10000000 R3=e000ed04
gdk3:R12=8002c5f LR=8000e87
gdk3:PC=2000001c PSR=0
gdk3:HFSR=deadd0d0 CFSR=20000
值得说明的是,FreeRTOS在异常处理方面是比较弱的,一旦出问题,一般的做法就是跳到一个死循环,根本没有NT那样的蓝屏,与Linux的Panic机制相比,也要弱很多。
大家现在看到的打印信息还是我后来加进去的。
上面错误信息的根据是硬件的CFSR寄存器,这个寄存器把M的异常分为三大类:UsageFault,BusFault和MemManage,即用法错、总线错和内存管理(错)。
那么,什么是用法错呢?简单说,就是”软件把硬件用错了“。是站在硬件的立场来说的:”你们这帮程序员啊,不学无术,乱写代码,这么好的硬件用不好,不按规则做,到处犯规...读书时不认真学习,毕业了不勤奋实践,真是拿你们没有办法^_^“
对于”用法错“,M核的手册上又细分为多种,每个比特位代表一种。我们遇到的是所谓为INVSTATE,即无效状态位。
简单来说,ARM的指令分为四字节的普通指令,基于2或4字节的THUMB指令。为了区分这两种指令,便搞出了一系列规矩,比如程序状态寄存器(PSR)里有个T位(THUMB),T位为1时,代表要执行的是THUMB指令,T位为0时代表要执行的是标准。
对于GDK3使用的M核来说,它只支持THUMB指令。因此T位应该总是为1,如果谁将其改写为0,那么就是捣乱,就是闹事,就是故意和硬件团队过不去。得到的后果就是UsageFault,准确的说是UsageFault.INVSTATE。
使用ARM文档的话,这个错误的原因是:
Instruction executed with invalid EPSR.T or EPSR.IT field.
上面的信息主要来自ARM的ARM(架构参考手册)。根据多年的经验,我把这个异常的理论原因搞得比较清楚了。但是,这与实际解决问题还有很大的距离。
接下来需要寻找是哪里的代码要把EPSR的T位清零。要知道像EPSR这样的寄存器,很多位是不可以直接读写的,是处理器自动维护的。
用ARM的ARM里的话来说:
The EPSR fields are read-only. The processor ignores any attempt by privileged software to write to them.
00000008 08001c51 lk!EXTI2_IRQHandler+0x1 [../../startup/startup_GDK3.s @ 78]
0000000c 08001b7d lk!HardFault_Handler+0x1 [../../startup/exceptions.c @ 279]
00000010 08001ba1 lk!MemManage_Handler+0x1 [../../startup/exceptions.c @ 294]
00000014 08001bc5 lk!BusFault_Handler+0x1 [../../startup/exceptions.c @ 309]
00000018 08001be9 lk!UsageFault_Handler+0x1 [../../startup/exceptions.c @ 324]
naked是什么?凡是学点英文的都知道。没学过英文甚至也知道。
但是对于很多程序员来说,虽然知道naked的字面意思,但却可能不了解它的用法。因为这个编程知识真是用的不多。
长话短说,普通的函数都不是naked,都是穿着衣服的。所谓衣服就是函数开头和结尾的包装,一般称为函数的序言和结语,是编译器自动加上的。
而所谓的naked函数,就是不带包装的,没穿衣服的。
函数序言和结语都做什么呢?主要是操作栈。也就是在开头把要保存的数据压进栈,在末尾时再弹出。分配和释放局部变量一般也是这时做的。
对于普通的函数,编译器都会给他们加上包装。而对于异常处理函数来说,CPU在处理异常时,直接飞进这些函数,这时要求精确的操作栈,这也是为什么要用汇编语言的原因。
那么为什么漏了naked属性呢?
准确地说,不是漏,而是误会了。因为naked属性并不常用,它的写法不是C语言的标准,所以不同编译器的写法不同。
以本例来说,本来使用的代码是这样写的:
void xPortPendSVHandler( void ) __attribute__( ( naked ) );
void xPortSysTickHandler( void );
void vPortSVCHandler( void ) __attribute__( ( naked ) );
也就是把__attribute__( ( naked ) )放在函数原型声明语句里,而且是放在函数名后面。
但是实际情况证明这种写法无效,产生的目标代码是假的nake,编译器还会加上栈操作。比如:
lk!PendSV_Handler [portable/RVDS/ARM_CM3/port.c @ 404]:
8001550 b480 push {r7}
8001552 af00 add r7, sp, #0
而真正的第一条语句应该是mrs。
void xPortPendSVHandler( void )
{
/* This is a naked function. */
__asm volatile
(
" mrs r0, psp \n"
" isb \n"
" \n"
对于GCC编译器,正确的写法是在函数开头这样写:
__attribute__( ( naked ) ) void xPortPendSVHandler( void )
{
/* This is a naked function. */
__asm volatile
这样纠正写法后,一切就都正常了!
我与文波是在2016年12月的庐山研习班上认识的。查找相册,刚好找到当年在五老峰前的一张合影(后排右一为文波)。
时光匆匆,转眼6年过去了。我和文波如今是经常聊天的好朋友,在今年下半年的IOT课程上,我们一起合作隔周一次的试验部分。这次在我新冠阳了的时候,他侠客一般果断出手,帮我解决了一个大bug,可谓雪中送炭。
2022年就要过去了,对于所有人来说,这都是非常难忘的一年。困难终将过去,未来无限美好,在此衷心祝愿格友们新年快乐,2023年里天天进步。
正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生
扫描下方二维码或者在微信中搜索“盛格塾”小程序,可以阅读更多文章和有声读物
也欢迎关注格友公众号